package org.apache.tapestry5.internal.portlet.services;

import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.portlet.BaseURL;
import javax.portlet.PortletException;
import javax.portlet.PortletRequest;
import javax.portlet.PortletURL;
import javax.portlet.ResourceURL;
import javax.portlet.WindowState;

import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.EventConstants;
import org.apache.tapestry5.EventContext;
import org.apache.tapestry5.Link;
import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.internal.InternalConstants;
import org.apache.tapestry5.internal.portlet.services.response.PortletResponse;
import org.apache.tapestry5.internal.services.RequestSecurityManager;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.portlet.services.PortletInterfaceProvider;
import org.apache.tapestry5.runtime.Component;
import org.apache.tapestry5.services.ComponentClassResolver;
import org.apache.tapestry5.services.ComponentEventLinkEncoder;
import org.apache.tapestry5.services.ComponentEventRequestParameters;
import org.apache.tapestry5.services.ComponentSource;
import org.apache.tapestry5.services.ContextPathEncoder;
import org.apache.tapestry5.services.LocalizationSetter;
import org.apache.tapestry5.services.PageRenderRequestParameters;
import org.apache.tapestry5.services.PersistentLocale;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ComponentEventLinkEncoderImplPortlet implements ComponentEventLinkEncoder {

    private final Logger                 _logger               = LoggerFactory
                                                                   .getLogger(ComponentEventLinkEncoderImplPortlet.class);

    private final ComponentClassResolver componentClassResolver;

    private final ContextPathEncoder     contextPathEncoder;

    private final LocalizationSetter     localizationSetter;

    private final PortletRequest         portletRequest;

    private final PortletResponse        portletResponse;

    private final RequestSecurityManager requestSecurityManager;

    private final PersistentLocale       persistentLocale;

    private final ComponentSource        componentSource;

    private final boolean                encodeLocaleIntoPath;

    private static final int             BUFFER_SIZE           = 100;

    private static final char            SLASH                 = '/';

    private static final String          PAGE_CONTEXT_NAME_MOD = "?t:ac=";

    // A beast that recognizes all the elements of a path in a single go.
    // We skip the leading slash, then take the next few terms (until a dot or a colon)
    // as the page name.  Then there's a sequence that sees a dot
    // and recognizes the nested component id (which may be missing), which ends
    // at the colon, or at the slash (or the end of the string).  The colon identifies
    // the event name (the event name is also optional).  A valid path will always have
    // a nested component id or an event name (or both) ... when both are missing, then the
    // path is most likely a page render request.  After the optional event name,
    // the next piece is the action context, which is the remainder of the path.

    private final Pattern                PATH_PATTERN          = Pattern.compile(

                                                               "^/" + // The leading slash is recognized but skipped
                                                                   "(((\\w+)/)*(\\w+))" + // A series of folder names leading up to the page name, forming the logical page name
                                                                   "(\\.(\\w+(\\.\\w+)*))?" + // The first dot separates the page name from the nested component id
                                                                   "(\\:(\\w+))?" + // A colon, then the event type
                                                                   "(/(.*))?", //  A slash, then the action context
                                                                   Pattern.COMMENTS);

    // Constants for the match groups in the above pattern.
    private static final int             LOGICAL_PAGE_NAME     = 1;

    private static final int             NESTED_ID             = 6;

    private static final int             EVENT_NAME            = 9;

    private static final int             CONTEXT               = 11;

    private final Response               response;

    public ComponentEventLinkEncoderImplPortlet(ComponentClassResolver componentClassResolver,
                                                ContextPathEncoder contextPathEncoder,
                                                LocalizationSetter localizationSetter,

                                                Response response,

                                                @PortletInterfaceProvider PortletRequest portletRequest,
                                                PortletResponse portletResponse,

                                                RequestSecurityManager requestSecurityManager,
                                                PersistentLocale persistentLocale,
                                                ComponentSource componentSource,

                                                @Symbol(SymbolConstants.ENCODE_LOCALE_INTO_PATH) boolean encodeLocaleIntoPath) {
        this.componentClassResolver = componentClassResolver;
        this.contextPathEncoder = contextPathEncoder;
        this.localizationSetter = localizationSetter;
        this.response = response;
        this.portletRequest = portletRequest;
        this.portletResponse = portletResponse;
        this.requestSecurityManager = requestSecurityManager;
        this.persistentLocale = persistentLocale;
        this.componentSource = componentSource;
        this.encodeLocaleIntoPath = encodeLocaleIntoPath;
    }

    @Override
    public Link createPageRenderLink(PageRenderRequestParameters parameters) {

        PortletURL portletURL = null;
        try {
            portletURL = this.portletResponse.createRenderURL();
        }
        catch (Throwable e) {
            e.printStackTrace();
            this._logger.error("", e);
        }

        try {
            portletURL.setPortletMode(this.portletRequest.getPortletMode());
            portletURL.setWindowState(WindowState.NORMAL);
        }
        catch (PortletException pe) {
            this._logger.error(PortletServicesMessages.errorProcessingRender(pe));
        }

        // Build up the absolute URI.

        String activePageName = parameters.getLogicalPageName();

        StringBuilder builder = new StringBuilder(ComponentEventLinkEncoderImplPortlet.BUFFER_SIZE);

        this.encodeLocale(builder);

        builder.append(ComponentEventLinkEncoderImplPortlet.SLASH);

        String encodedPageName = this.encodePageName(activePageName);

        builder.append(encodedPageName);

        this.appendContext(encodedPageName.length() > 0, parameters.getActivationContext(), builder);

        return new LinkImplPortlet(portletURL, builder.toString(), false,
            this.requestSecurityManager.checkPageSecurity(activePageName), this.response, this.contextPathEncoder);
    }

    private void encodeLocale(StringBuilder builder) {
        if (this.encodeLocaleIntoPath) {
            Locale locale = this.persistentLocale.get();

            if (locale != null) {
                builder.append(ComponentEventLinkEncoderImplPortlet.SLASH);
                builder.append(locale.toString());
            }
        }
    }

    private String encodePageName(String pageName) {
        if (pageName.equalsIgnoreCase("index")) {
            return "";
        }

        String encoded = pageName.toLowerCase();

        if (!encoded.endsWith("/index")) {
            return encoded;
        }

        return encoded.substring(0, encoded.length() - 6);
    }

    @Override
    public Link createComponentEventLink(ComponentEventRequestParameters parameters, boolean forForm) {

        StringBuilder builder = new StringBuilder(ComponentEventLinkEncoderImplPortlet.BUFFER_SIZE);

        // Build up the absolute URI.

        String activePageName = parameters.getActivePageName();
        String containingPageName = parameters.getContainingPageName();
        String eventType = parameters.getEventType();

        String nestedComponentId = parameters.getNestedComponentId();
        boolean hasComponentId = InternalUtils.isNonBlank(nestedComponentId);

        this.encodeLocale(builder);

        builder.append(ComponentEventLinkEncoderImplPortlet.SLASH);
        builder.append(activePageName.toLowerCase());

        if (hasComponentId) {
            builder.append('.');
            builder.append(nestedComponentId);
        }

        if (!hasComponentId || !eventType.equals(EventConstants.ACTION)) {
            builder.append(":");
            builder.append(this.encodePageName(eventType));
        }

        this.appendContext(true, parameters.getEventContext(), builder);

        // portlet related code:
        boolean isXHR = this.isXHR(containingPageName, nestedComponentId, eventType);

        BaseURL url = this.buildURL(eventType, nestedComponentId, isXHR);

        Link result = new LinkImplPortlet(url, builder.toString(), forForm,
            this.requestSecurityManager.checkPageSecurity(activePageName), this.response, this.contextPathEncoder);

        EventContext pageActivationContext = parameters.getPageActivationContext();

        if (pageActivationContext.getCount() != 0) {
            // Reuse the builder
            builder.setLength(0);
            this.appendContext(true, pageActivationContext, builder);

            // Omit that first slash
            result.addParameter(InternalConstants.PAGE_CONTEXT_NAME, builder.substring(1));
        }

        // TAPESTRY-2044: Sometimes the active page drags in components from another page and we
        // need to differentiate that.

        if (!containingPageName.equalsIgnoreCase(activePageName)) {
            result.addParameter(InternalConstants.CONTAINER_PAGE_NAME, this.encodePageName(containingPageName));
        }

        return result;
    }

    private boolean isXHR(String containingPageName, String nestedComponentId, String eventType) {
        // get the element for which we are creating the Link to see if it's triggering a zone (i.e. is an AJAX event).

        // TODO: EventLink is using the containing page's ComponentResource to generate the url so nestedComponentId is ALWAYS null!
        if (InternalUtils.isBlank(nestedComponentId)) {
            return false;
        }

        Component component = this.componentSource.getComponent(containingPageName + ":" + nestedComponentId);

        ComponentResources componentResources = component.getComponentResources();

        return componentResources.isBound("zone");
    }

    private BaseURL buildURL(String eventType, String nestedComponentId, boolean isXHR) {

        if (this.isResourceServingEvent(eventType, nestedComponentId) || isXHR) {
            this._logger.info("ResourceURL");

            ResourceURL url = this.portletResponse.createResourceURL();
            if (isXHR) {
                // HACK:
                // this is used by Request.isXHR() to determine whether it's an AJAX request or not. 
                url.setParameter("AJAX_REQUEST", "true");
            }

            return url;
        }
        else {
            PortletURL url = this.portletResponse.createActionURL();
            try {
                url.setPortletMode(this.portletRequest.getPortletMode());
                url.setWindowState(this.portletRequest.getWindowState());
            }
            catch (PortletException e) {
                // TODO Auto-generated catch block
                this._logger.error(PortletServicesMessages.errorProcessingRender(e));
            }
            return url;
        }
    }

    private boolean isResourceServingEvent(String event, String nestedComponentId) {
        // HACK:
        // we are using events which end with "resource" to serve resources (most likely StreamResponse)
        // alternatively we support ActionLinks with ids that end with "resource"
        boolean resourceAction = event.equals(EventConstants.ACTION)
            && nestedComponentId.toLowerCase().endsWith("resource");
        return event.toLowerCase().endsWith("resource") || resourceAction;
    }

    @Override
    public ComponentEventRequestParameters decodeComponentEventRequest(Request request) {
        String path = request.getPath();
        int indexOfPageContextName = path.indexOf(ComponentEventLinkEncoderImplPortlet.PAGE_CONTEXT_NAME_MOD);
        if (indexOfPageContextName != -1) {
            path = path.substring(0, indexOfPageContextName);
        }

        Matcher matcher = this.PATH_PATTERN.matcher(path);

        if (!matcher.matches()) {
            return null;
        }

        String nestedComponentId = matcher.group(ComponentEventLinkEncoderImplPortlet.NESTED_ID);

        String eventType = matcher.group(ComponentEventLinkEncoderImplPortlet.EVENT_NAME);

        if (nestedComponentId == null && eventType == null) {
            return null;
        }

        String activePageName = matcher.group(ComponentEventLinkEncoderImplPortlet.LOGICAL_PAGE_NAME);

        int slashx = activePageName.indexOf('/');

        String possibleLocaleName = slashx > 0 ? activePageName.substring(0, slashx) : "";

        if (this.localizationSetter.setLocaleFromLocaleName(possibleLocaleName)) {
            activePageName = activePageName.substring(slashx + 1);
        }

        if (!this.componentClassResolver.isPageName(activePageName)) {
            return null;
        }

        EventContext eventContext = this.contextPathEncoder.decodePath(matcher
            .group(ComponentEventLinkEncoderImplPortlet.CONTEXT));

        String pageContext;
        if (indexOfPageContextName != -1) {
            pageContext = request.getPath().substring(indexOfPageContextName + 6);
        }
        else {
            pageContext = null;
        }

        EventContext activationContext = this.contextPathEncoder.decodePath(pageContext);

        // The event type is often omitted, and defaults to "action".

        if (eventType == null) {
            eventType = EventConstants.ACTION;
        }

        if (nestedComponentId == null) {
            nestedComponentId = "";
        }

        String containingPageName = request.getParameter(InternalConstants.CONTAINER_PAGE_NAME);

        if (containingPageName == null) {
            containingPageName = activePageName;
        }

        return new ComponentEventRequestParameters(activePageName, containingPageName, nestedComponentId, eventType,
            activationContext, eventContext);
    }

    @Override
    public PageRenderRequestParameters decodePageRenderRequest(Request request) {
        // The extended name may include a page activation context. The trick is
        // to figure out where the logical page name stops and where the
        // activation context begins. Further, strip out the leading slash.

        String path = request.getPath();

        // TAPESTRY-1343: Sometimes path is the empty string (it should always be at least a slash,
        // but Tomcat may return the empty string for a root context request).

        String extendedName = path.length() == 0 ? path : path.substring(1);

        // Ignore trailing slashes in the path.
        while (extendedName.endsWith("/")) {
            extendedName = extendedName.substring(0, extendedName.length() - 1);
        }

        int slashx = extendedName.indexOf('/');

        // So, what can we have left?
        // 1. A page name
        // 2. A locale followed by a page name
        // 3. A page name followed by activation context
        // 4. A locale name, page name, activation context
        // 5. Just activation context (for root Index page)
        // 6. A locale name followed by activation context

        String possibleLocaleName = slashx > 0 ? extendedName.substring(0, slashx) : extendedName;

        if (this.localizationSetter.setLocaleFromLocaleName(possibleLocaleName)) {
            extendedName = slashx > 0 ? extendedName.substring(slashx + 1) : "";
        }

        slashx = extendedName.length();
        boolean atEnd = true;

        while (slashx > 0) {
            String pageName = extendedName.substring(0, slashx);
            String pageActivationContext = atEnd ? "" : extendedName.substring(slashx + 1);

            PageRenderRequestParameters parameters = this.checkIfPage(pageName, pageActivationContext);

            if (parameters != null) {
                return parameters;
            }

            // Work backwards, splitting at the next slash.
            slashx = extendedName.lastIndexOf('/', slashx - 1);

            atEnd = false;
        }

        // OK, maybe its all page activation context for the root Index page.

        return this.checkIfPage("", extendedName);
    }

    private PageRenderRequestParameters checkIfPage(String pageName, String pageActivationContext) {
        if (!this.componentClassResolver.isPageName(pageName)) {
            return null;
        }

        EventContext activationContext = this.contextPathEncoder.decodePath(pageActivationContext);

        return new PageRenderRequestParameters(pageName, activationContext);
    }

    public void appendContext(boolean seperatorRequired, EventContext context, StringBuilder builder) {
        String encoded = this.contextPathEncoder.encodeIntoPath(context);

        if (encoded.length() > 0) {
            if (seperatorRequired) {
                builder.append(ComponentEventLinkEncoderImplPortlet.SLASH);
            }

            builder.append(encoded);
        }
    }
}
